package com.citahub.link;

import com.citahub.cita.protocol.CITAj;
import com.citahub.cita.protocol.core.DefaultBlockParameterName;
import com.citahub.cita.protocol.core.DefaultBlockParameterNumber;
import com.citahub.cita.protocol.core.methods.request.Call;
import com.citahub.cita.protocol.core.methods.response.AppBlock;
import com.citahub.cita.protocol.core.methods.response.AppBlockNumber;
import com.citahub.cita.protocol.core.methods.response.AppGetTransactionReceipt;
import com.citahub.cita.protocol.core.methods.response.AppSendTransaction;
import com.citahub.cita.protocol.core.methods.response.AppTransaction;
import com.citahub.cita.protocol.http.HttpService;
import com.citahub.config.ResourceStubConfig;
import com.citahub.config.ResourcesStubConfigParser;
import com.citahub.constant.TransactionConstant;
import com.citahub.constant.TransactionConstant.Event;
import com.citahub.constant.TransactionConstant.Result;
import com.citahub.contract.ContractCall;
import com.citahub.util.InternalBlock;
import com.citahub.util.Utils;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;

import com.google.common.collect.Maps;
import link.luyu.protocol.common.STATUS;
import link.luyu.protocol.link.Connection;
import org.springframework.util.CollectionUtils;

public class CITAConnection implements Connection {

    private CITAj citAj;

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    private String chainDir;

    private String chainType;

    public CITAConnection(String chainUrl) {
        this.citAj = CITAj.build(new HttpService(chainUrl));
    }

    public void start() throws RuntimeException {

    }

    public void stop() throws RuntimeException {

    }

    public CITAConnection(Map<String, Object> properties) throws Exception {
        Map<String, Object> common = (Map<String, Object>) properties.get("common");
        if (common == null) {
            throw new Exception("[common] item not found");
        }

        String chainUrl = (String) common.get("chainUrl");
        if (chainUrl == null) {
            throw new Exception("\"chainUrl\" item not found");
        }

        this.citAj = CITAj.build(new HttpService(chainUrl));

        this.chainDir = (String) properties.get("chainDir");
        this.chainType = (String) common.get("type");
    }

    @Override
    public void asyncSend(String path, int type, byte[] data, Callback callback) {
        switch (type) {
            case TransactionConstant.Type.SEND_TRANSACTION:
                {
                    try {
                        AppSendTransaction appSendTransaction =
                                citAj.appSendRawTransaction(new String(data)).send();
                        if (appSendTransaction.getError() != null) {
                            String message = appSendTransaction.getError().getMessage();
                            callback.onResponse(TransactionConstant.Result.ERROR, message, null);
                        } else {
                            callback.onResponse(
                                    Result.SUCCESS,
                                    "Success",
                                    appSendTransaction
                                            .getSendTransactionResult()
                                            .getHash()
                                            .getBytes());
                        }
                    } catch (IOException e) {
                        callback.onResponse(TransactionConstant.Result.ERROR, e.getMessage(), null);
                    }
                    break;
                }

            case TransactionConstant.Type.CALL_TRANSACTION:
                {
                    try {
                        ContractCall contractCall =
                                OBJECT_MAPPER.readValue(data, ContractCall.class);
                        Call call =
                                new Call(
                                        contractCall.getSender(),
                                        contractCall.getContract(),
                                        contractCall.getData());
                        String result =
                                citAj.appCall(call, DefaultBlockParameterName.PENDING)
                                        .send()
                                        .getValue();
                        callback.onResponse(Result.SUCCESS, "Success", result.getBytes());
                    } catch (IOException e) {
                        callback.onResponse(
                                Result.ERROR, "call failed,error" + e.getMessage(), null);
                    }
                    break;
                }

            case TransactionConstant.Type.GET_TRANSACTION_RECEIPT:
                {
                    try {
                        AppGetTransactionReceipt receipt =
                                citAj.appGetTransactionReceipt(new String(data)).send();
                        if (receipt.hasError()) {
                            callback.onResponse(
                                    TransactionConstant.Result.ERROR,
                                    receipt.getRawResponse(),
                                    null);
                            return;
                        }
                        callback.onResponse(
                                TransactionConstant.Result.SUCCESS,
                                "Success",
                                OBJECT_MAPPER.writeValueAsBytes(receipt.getTransactionReceipt()));
                    } catch (IOException e) {
                        callback.onResponse(TransactionConstant.Result.ERROR, e.getMessage(), null);
                    }
                    break;
                }
            case TransactionConstant.Type.GET_ABI:
                {
                    try {
                        String abi = citAj.appGetAbi(path, DefaultBlockParameterName.PENDING)
                                .send()
                                .getAbi();
                        callback.onResponse(
                                TransactionConstant.Result.SUCCESS,
                                "Success",
                                abi.getBytes(StandardCharsets.UTF_8));
                    } catch (IOException e) {
                        callback.onResponse(TransactionConstant.Result.ERROR, e.getMessage(), null);
                    }
                    break;
                }
            case TransactionConstant.Type.GET_BLOCK_NUMBER:
                {
                    try {
                        AppBlockNumber ret = citAj.appBlockNumber().send();
                        long blockNumber = ret.getBlockNumber().longValue();

                        callback.onResponse(
                                TransactionConstant.Result.SUCCESS,
                                "Success",
                                Utils.longToBytes(blockNumber));
                    } catch (IOException e) {
                        callback.onResponse(TransactionConstant.Result.ERROR, e.getMessage(), null);
                    }
                    break;
                }
            case TransactionConstant.Type.GET_BLOCK_BY_NUMBER:
                {
                    long blockNumber = Utils.bytesToLong(data);
                    try {
                        AppBlock appBlock =
                                citAj.appGetBlockByNumber(new DefaultBlockParameterNumber(blockNumber), false)
                                        .send();
                        if (appBlock.isEmpty()) {
                            callback.onResponse(STATUS.INTERNAL_ERROR, "Block is empty", null);
                            return;
                        }
                        InternalBlock blk = new InternalBlock(appBlock);
                        callback.onResponse(STATUS.OK, "Success", Utils.toByteArray(blk));
                    } catch (IOException e) {
                        callback.onResponse(STATUS.CONNECTION_EXCEPTION, e.getMessage(), null);
                    }
                    break;
                }
            case TransactionConstant.Type.GET_BLOCK_BY_HASH:
                {
                    String blockHash = path;
                    try {
                        AppBlock appBlock = citAj.appGetBlockByHash(blockHash, false).send();
                        if (appBlock.isEmpty()) {
                            callback.onResponse(STATUS.INTERNAL_ERROR, "Block is empty", null);
                            return;
                        }
                        InternalBlock blk = new InternalBlock(appBlock);
                        callback.onResponse(STATUS.OK, "Success", Utils.toByteArray(blk));
                    } catch (IOException e) {
                        callback.onResponse(STATUS.CONNECTION_EXCEPTION, e.getMessage(), null);
                    }
                    break;
                }
            case TransactionConstant.Type.GET_TRANSACTION:
                {
                  try {
                    AppTransaction transaction =
                              citAj.appGetTransactionByHash(new String(data)).send();
                      if (transaction.hasError()) {
                          callback.onResponse(
                                  TransactionConstant.Result.ERROR,
                                  transaction.getRawResponse(),
                                  null);
                          return;
                      }
                      callback.onResponse(
                              TransactionConstant.Result.SUCCESS,
                              "Success",
                              OBJECT_MAPPER.writeValueAsBytes(transaction.getTransaction()));
                  } catch (IOException e) {
                      callback.onResponse(TransactionConstant.Result.ERROR, e.getMessage(), null);
                  }
                  break;
                }
            case TransactionConstant.Type.GET_LIST_RESOURCES:
                {
                  try {
                      ResourceStubConfig configPath = new ResourcesStubConfigParser(chainDir).config();
                      List<ResourceStubConfig.Resource> resources = configPath.getResources();
                      List<link.luyu.protocol.network.Resource> resourceList = Lists.newArrayList();
                      if (!CollectionUtils.isEmpty(resources)) {
                          final String chainPath =
                                  null == configPath.getChainPath() ? "payment.cita" : configPath.getChainPath();
                          resources.forEach(r -> {
                              link.luyu.protocol.network.Resource resource = new link.luyu.protocol.network.Resource();
                              resource.setPath(
                                      new StringBuffer().append(chainPath).append(".").append(r.getName()).toString());
                              resource.setType(null == chainType ? "CITA" : chainType);
                              resource.setMethods(null == r.getMethods() ? new String[] {} : r.getMethods());
                              Map<String, Object> properties = Maps.newHashMap();
                              properties.put("contractAddress", r.getValue());
                              resource.setProperties(properties);
                              resourceList.add(resource);
                          });
                      }
                      callback.onResponse(STATUS.OK, "Success", OBJECT_MAPPER.writeValueAsBytes(resourceList));
                  } catch (IOException e) {
                      callback.onResponse(TransactionConstant.Result.ERROR, e.getMessage(), null);
                  }
                  break;
                }
            default:
                {
                    callback.onResponse(
                            TransactionConstant.Result.ERROR, "Unrecognized type of " + type, null);
                    break;
                }
        }
    }

    @Override
    public void subscribe(int type, byte[] data, Callback callback) {
        switch (type) {
            case Event.EVENT_NEW_BLOCK:
                {
                    break;
                }

            case Event.EVENT_RESOURCES_CHANGED:
                {
                    break;
                }
            default:
        }
    }
}
